package org.apache.ocean.main.search;

import java.io.IOException;

import org.apache.lucene.index.FilterIndexReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.HitCollector;
import org.apache.lucene.search.Scorer;
import org.apache.lucene.search.Weight;
import org.apache.ocean.main.IndexSchema;
import org.apache.ocean.main.Index.IndexSnapshot;
import org.apache.ocean.main.search.Filters.Distance;
import org.apache.ocean.main.search.cache.DocSetCacheKey;
import org.apache.ocean.main.search.cache.FieldCacheKey.FieldIndex;
import org.apache.ocean.main.util.Constants;
import org.apache.ocean.main.util.MutableInt;
import org.apache.ocean.main.util.Timeout;
import org.apache.ocean.main.util.Timeout.TimeoutRuntimeException;
import org.apache.ocean.util.DistanceUtils;
import org.apache.ocean.util.DocSet;

public class IndexSearch extends Search {
	private final IndexSnapshot indexSnapshot;
	private int numDocs;
	private final Sort sort;
	private final Weight weight;
	private final Timeout timeout;
	private final IndexSchema.Category indexSchemaCategory;
	private final DistanceOperation distanceOperation;
	private final DocSet filterDocSet;
	private DocSet queryDocSet;

	public IndexSearch(SearchQuery searchQuery, IndexSnapshot indexSnapshot) throws Exception {
		super(searchQuery);
		this.indexSnapshot = indexSnapshot;
		indexSchemaCategory = indexSnapshot.getIndex().getIndexSchemaCategory();
		weight = searchQuery.getWeight();
		numDocs = searchQuery.getNumDocs();
		sort = searchQuery.getSort();
		timeout = searchQuery.getTimeout();
		Filters filters = searchQuery.getFilters();
		distanceOperation = getDistanceOperation(filters, indexSnapshot);
		filterDocSet = getFilterDocSet(filters, timeout);
	}

	public Result<ScoreDoc> call() throws Exception {
		if (searchQuery.hasSort()) {
			SearchBySort searchBySort = new SearchBySort();
			return searchBySort.search();
		} else {
			SearchByScore searchByScore = new SearchByScore();
			return searchByScore.search();
		}
	}

	/**
	 * makes doc set seem deleted
	 */
	public static class SearchIndexReader extends FilterIndexReader {
		private DocSet docSet;
		private Timeout timeout;

		public SearchIndexReader(IndexReader indexReader, DocSet docSet, Timeout timeout) {
			super(indexReader);
			if (docSet == null)
				throw new IllegalArgumentException("docSet null");
			this.docSet = docSet;
			this.timeout = timeout;
		}

		public boolean isDeleted(int doc) {
			if (timeout != null && !timeout.isOK()) {
				throw new TimeoutRuntimeException("", timeout);
			}
			if (in.isDeleted(doc))
				return true;
			if (docSet != null) {
				return !docSet.exists(doc);
			} else {
				return true;
			}
		}

		public boolean hasDeletions() {
			return true;
		}
	}

	private class SearchBySort {
		public Result<ScoreDoc> search() throws Exception {
			long startTime = Timeout.TIMER_THREAD.getTime();
			final MutableInt numFound = new MutableInt(0);
			final FieldDocSortQueue fieldDocSortQueue = new FieldDocSortQueue(sort, numDocs, indexSnapshot);
			IndexReader snapshotIndexReader = indexSnapshot.getIndexReader();
			IndexReader indexReader = new SearchIndexReader(snapshotIndexReader, filterDocSet, timeout);
			Scorer scorer = weight.scorer(indexReader);
			scorer.score(new HitCollector() {
				public void collect(int doc, float score) {
					Double docDistanceMiles = null;
					if (distanceOperation != null) {
						docDistanceMiles = distanceOperation.getDocDistance(doc);
						if (docDistanceMiles == null)
							return;
					}
					FieldDoc fieldDoc = new FieldDoc(doc, score, docDistanceMiles, indexSnapshot, null);
					fieldDocSortQueue.insert(fieldDoc);
					numFound.value++;
				}
			});
			ScoreDoc[] scoreDocs = toArray(fieldDocSortQueue);
			for (ScoreDoc scoreDoc : scoreDocs) {
				FieldDoc fieldDoc = (FieldDoc) scoreDoc;
				fieldDocSortQueue.fillFields(fieldDoc);
			}
			fillCrowdValues(searchQuery.getCrowds(), scoreDocs);
			long elapsed = Timeout.TIMER_THREAD.getTime() - startTime;
			return new Result<ScoreDoc>(searchQuery.getStart(), scoreDocs, (int) numFound.value, elapsed);
		}
	}

	private class SearchByScore {
		float minScore;

		public SearchByScore() {
		}

		public Result<ScoreDoc> search() throws Exception {
			long startTime = Timeout.TIMER_THREAD.getTime();
			minScore = Float.NEGATIVE_INFINITY;
			final MutableInt numFound = new MutableInt(0);
			final ScorePriorityQueue scorePriorityQueue = new ScorePriorityQueue(numDocs);
			IndexReader snapshotIndexReader = indexSnapshot.getIndexReader();
			IndexReader indexReader = new SearchIndexReader(snapshotIndexReader, filterDocSet, timeout);
			Scorer scorer = weight.scorer(indexReader);
			scorer.score(new HitCollector() {
				public void collect(int doc, float score) {
					if (score > 0.0f) {
						if (numFound.value++ < numDocs || score >= minScore) {
							Double docDistanceMiles = null;
							if (distanceOperation != null) {
								docDistanceMiles = distanceOperation.getDocDistance(doc);
								if (docDistanceMiles == null)
									return;
							}
							ScoreDoc scoreDoc = new ScoreDoc(doc, score, docDistanceMiles, null, indexSnapshot);
							scorePriorityQueue.insert(scoreDoc);
							if (queryDocSet != null)
								queryDocSet.add(doc);
							minScore = ((ScoreDoc) scorePriorityQueue.top()).score;
						}
					}
				}
			});
			ScoreDoc[] scoreDocs = toArray(scorePriorityQueue);
			fillCrowdValues(searchQuery.getCrowds(), scoreDocs);
			long elapsed = Timeout.TIMER_THREAD.getTime() - startTime;
			return new Result<ScoreDoc>(searchQuery.getStart(), scoreDocs, (int) numFound.value, elapsed);
		}
	}

	private static DistanceOperation getDistanceOperation(Filters filters, IndexSnapshot indexSnapshot) throws Exception {
		if (filters != null && filters.hasDistance()) {
			DistanceOperation distanceOperation = new DistanceOperation(); 
			distanceOperation.distance = filters.getDistance();
			distanceOperation.latitudeFieldIndex = indexSnapshot.getFieldIndex(Constants.LATITUDE);
			distanceOperation.longitudeFieldIndex = indexSnapshot.getFieldIndex(Constants.LONGITUDE);
			return distanceOperation;
		}
		return null;
	}

	private DocSet getFilterDocSet(Filters filters, Timeout timeout) throws Exception {
		if (filters == null || filters.size() == 0)
			return null;
		DocSetCacheKey cacheKey = filters.getCacheKey();
		if (cacheKey == null)
			return null;
		return indexSnapshot.getDocSet(cacheKey, timeout);
	}

	private static class DistanceOperation {
		public Distance distance;
		public FieldIndex<Double> latitudeFieldIndex;
		public FieldIndex<Double> longitudeFieldIndex;

		public Double getDocDistance(int doc) {
			double docDistance = getDistanceMiles(doc);
			if (docDistance > distance.getDistanceMiles())
				return null;
			return docDistance;
		}

		public double getDistanceMiles(int doc) {
			return createDistanceCurved(latitudeFieldIndex.getValue(doc), longitudeFieldIndex.getValue(doc), distance);
		}
	}

	public static double createDistanceCurved(Double latitude, Double longitude, Distance distance) {
		return DistanceUtils.getDistanceMi(distance.getLatitude(), distance.getLongitude(), latitude.doubleValue(), longitude.doubleValue());
	}
}
